{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 函数定义再探"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们已经见过不少函数，也自己写过一些函数。我们已经理解函数的概念来自代数：从**输入参数**出发，**计算**出函数的**返回值**；我们也知道可以用 `def foo():` 来定义函数。其实函数的定义非常复杂，我们不太能够在第一次介绍时就讲清楚，所以之前我们就采取“先引入用起来”的方法，这也是一种知识上的“提前引用”。\n",
    "\n",
    "这一章我们就围绕函数定义深入看看。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 为函数命名"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "哪怕一个函数内部什么都不干，它也得有个名字，然后名字后面要加上圆括号 `()`，以明示它是个函数，而不是某个变量。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "def do_nothing():\n",
    "    pass\n",
    "\n",
    "do_nothing()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这就是个“什么也不干”的函数，关键字 `pass` 就是什么也不干的意思。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "给函数命名（给变量命名也一样）需要遵循的一些规则如下：\n",
    "* 首先，名称不能以数字开头，能用在名称开头的只有大小写字母和下划线 `_`；\n",
    "* 其次，名称中不能有空格，如果一个名字里有好几个词汇，可以用下划线来分割（`do_nothing`），也可以用所谓 *Camel Case* 风格（*doNothing*），习惯上更推荐使用下划线；\n",
    "* 最后，绝对不能与 Python 语言的**关键字**（*keyword*）重复。\n",
    "\n",
    "最后这一条，关键字也叫**保留字**（*reserved*），是编程语言保护起来内部使用的，如果程序用这些词儿做变量或者函数或者类型名字，编译器或者解释器就无法正确工作了。Python 提供了一个模块叫 `keyword` 来帮助我们了解语言有哪些关键字："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['False',\n",
       " 'None',\n",
       " 'True',\n",
       " 'and',\n",
       " 'as',\n",
       " 'assert',\n",
       " 'async',\n",
       " 'await',\n",
       " 'break',\n",
       " 'class',\n",
       " 'continue',\n",
       " 'def',\n",
       " 'del',\n",
       " 'elif',\n",
       " 'else',\n",
       " 'except',\n",
       " 'finally',\n",
       " 'for',\n",
       " 'from',\n",
       " 'global',\n",
       " 'if',\n",
       " 'import',\n",
       " 'in',\n",
       " 'is',\n",
       " 'lambda',\n",
       " 'nonlocal',\n",
       " 'not',\n",
       " 'or',\n",
       " 'pass',\n",
       " 'raise',\n",
       " 'return',\n",
       " 'try',\n",
       " 'while',\n",
       " 'with',\n",
       " 'yield']"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import keyword\n",
    "keyword.kwlist"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`keyword.kwlist` 就是当前你使用的 Python 解释器中不可使用的关键字列表，如果我们不记得这个列表，可以随时用 `keyword.iskeyword('xxx')` 来查询某个词是不是关键字。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在程序里给变量、函数命名是个挺重要的事情，影响到程序的可读性，就像小说的语言，最好能有一种流畅清晰、又始终一致的**风格**（*style*）。为了让全世界的 Python 程序员都有相对一致的风格，Python 社区有专门的一套建议规范，放在专门维护 Python 语言特性的社区 [PEP](https://www.python.org/dev/peps/) 上：\n",
    "\n",
    "* [PEP 8 -- Style Guide for Python Code: Naming Conventions](https://www.python.org/dev/peps/pep-0008/#naming-conventions)\n",
    "\n",
    "> PEP，是 *Python enhancement proposal* 的缩写，每当有重要的语言特性新需求新想法，就放在这里，经过广大 Python 用户和开发者的讨论完善，在某个版本放进 Python 中。很多 PEP 早已从 *proposal* 毕业变成官方特性，但也还在这里保留着。PEP 8 就是一个古老的 *proposal*，现在已为大多数 Python 用户采纳。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 没有、一个和多个参数"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "函数可以没有参数，也可以有一个或者多个参数。\n",
    "\n",
    "没有参数就意味着，这个函数执行不依赖于输入，比如我们定义一个函数来在程序结束时打印一句退出提示："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Program exits. Bye.\n"
     ]
    }
   ],
   "source": [
    "def exit_info():\n",
    "    print('Program exits. Bye.')\n",
    "    \n",
    "exit_info()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "注意即使没有参数，无论定义还是调用时，函数名后面的括号都是不可省略的，这是函数身份的标志。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "函数也可以有多个参数，调用时输入参数的值是严格按照参数的顺序去匹配的。比如我们写一个函数输出某年到某年之间的所有闰年："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2000\n",
      "2004\n",
      "2008\n",
      "2012\n",
      "2016\n"
     ]
    }
   ],
   "source": [
    "def leap_years(begin, end):\n",
    "    year = begin\n",
    "    while year < end:\n",
    "        if (year % 4 == 0 and year % 100 != 0) or year % 400 == 0:\n",
    "            print(year)\n",
    "        year += 1\n",
    "        \n",
    "leap_years(2000, 2020)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "当我们调用 `leap_years(2000, 2020)` 时，输入两个参数值 2000 和 2020，按照顺序匹配函数定义 `leap_years(begin, end)`，于是 `begin = 2000` `end = 2020`。所以参数的顺序是不能搞错的，有些函数参数很多，要是开发过程中还调整过顺序的话，那简直就是灾难，所以一般情况下还是保持函数参数不要乱动为好。\n",
    "\n",
    "顺便说一句，判断闰年的算法虽然不难，但要写的简洁也不容易。建议你可以先自己思考和实现一遍，然后尝试搞清楚为啥上面代码里的那行 `if` 是对的。实际上闰年的判断有很多正确的写法，你应该尝试写出自己的版本并确认它的正确性。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 没有、一个和多个返回值"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "和参数一样，Python 的函数可以没有返回值，也可以有一个或者多个返回值。\n",
    "\n",
    "上面的 `exit_info` 和 `leap_year` 也是没有返回值的例子，它们的效果都通过 `print` 函数来体现。实际上没有返回语句的函数，等价于在其最后有一句 `return None`，表示函数返回了一个空值 `None`，`None` 在 Python 中是一个合法的值，表示什么都没有，它在逻辑上等价于 `False`："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "bool(None)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "所以即使没有返回值的函数，也可以用在 `if` 后面做逻辑表达式，不过我们并不推荐这么做，因为可读性很差。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "大部分情况下函数是有返回值的，因为绝大部分情况下函数的作用都是做“数据处理”，从输入出发得到输出。\n",
    "\n",
    "一般情况下函数都只有一个返回值，我们已经见过不少例子；但 Python 也允许多返回值，比如我们想用一个函数来计算两个整数相除的商和余数，可以这么写："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "8 2\n"
     ]
    }
   ],
   "source": [
    "def idiv(a, b):\n",
    "    quotient = a // b\n",
    "    remainder = a % b\n",
    "    return quotient, remainder\n",
    "\n",
    "q, r = idiv(50, 6)\n",
    "print(q, r)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "和多参数的情况类似，多返回值的情况下，赋值也是按照顺序匹配的，上面的代码中赋值语句左边的 `q` 匹配到第一个返回值，`r` 匹配第二个。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 函数内与函数外：变量的作用域"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "下面的代码经常会把人搞晕："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2\n",
      "1\n"
     ]
    }
   ],
   "source": [
    "def increase_one(n):\n",
    "    n += 1\n",
    "    return n\n",
    "\n",
    "n = 1\n",
    "print(increase_one(n))\n",
    "print(n)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "请你思考一下，为什么这段代码里的两个 `print` 函数输出分别是 2 和 1。\n",
    "\n",
    "这个问题就涉及到变量的**作用域**（*scope*）问题，也就是说在不同地方出现的同名变量和函数，可能是完全不同的两个东西：\n",
    "* 函数定义体中的变量的作用域是该函数内，程序的其他部分不知道其存在，这种变量叫**局部变量**（*local variable*）；函数的输入参数也是局部变量，也只在函数定义体中有效；\n",
    "* 不在任何函数、类定义体中的变量的作用域是全局的，在任何地方都可以访问，这种变量称为**全局变量**（*global variable*）；\n",
    "* 如果局部变量和全局变量同名，函数定义体内会优先局部变量，不会把它当做全局变量。\n",
    "\n",
    "这样我们就能理解上面代码输出的 2 和 1 了：\n",
    "* 第一个 `print()` 打印的是函数调用 `increase_one(n)` 的返回值，这个语句不在任何函数定义体中，所以它里面用到的变量都是全局变量：\n",
    "    * 在调用 `increase_one()` 时参数 `n`，按照作用域原理，是全局变量 `n` 当时的值，也就是 1；\n",
    "    * 在 `increase_one()` 函数定义内，参数 `n` 是输入参数即局部变量，带着传进来的值 1，经过加一之后返回，返回值是 2；\n",
    "    * `print` 打印这个返回值，输出 2；\n",
    "    * 这个过程中处理的都是局部变量，完全不影响全局变量 `n` 的值；\n",
    "* 第二个 `print()` 打印的是全局变量 `n` 的值，输出出 1。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "以上的文字，可能需要反复阅读若干遍；几遍下来，消除了疑惑，以后就彻底没问题了；若是这个疑惑并未消除，或者关键点并未消化，以后则会反复被这个疑惑所坑害，浪费无数时间。\n",
    "\n",
    "顺便说一句，上面这个例子用来说明作用域的概念很有用，但是平时写程序最好别这么写，减少重名的变量可以提升代码的清晰度和可读性。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "与此相关的，我们在介绍列表等数据容器时，会为上面的规则作出重要的补充，这里先留一个伏笔。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 带缺省值的参数"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们其实已经见过带缺省值的参数（*argument with default value*），这里我们更细致的看看这个特性。\n",
    "\n",
    "在函数定义中可以在某个参数后面用等号 `=` 给它一个缺省值，调用时可以省略传入这个参数的值，直接采用缺省值；当然也可以在调用时传入这个参数的值来覆盖掉缺省值。这种特性相当于给了这个函数两个版本，一个带某个参数，一个不带，不带的版本就当该参数是某个缺省值。看看下面的例子："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "def greeting(name, msg='Hi'):\n",
    "    print(f'{msg}, {name}!')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Hi, Neo!\n"
     ]
    }
   ],
   "source": [
    "greeting('Neo')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Good morning, Neo!\n"
     ]
    }
   ],
   "source": [
    "greeting('Neo', 'Good morning')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "一个函数可以有多个带缺省值的参数，但有一个限制：所有这些带缺省值的参数只能堆在参数表的最后，也就是说你定义的参数表里，出现一个带缺省值的参数，则它后面的都必须带缺省值。如果把上面的 `greeting()` 函数的两个参数调换一下，会扔出一个 `SyntaxError: non-default argument follows default argument` 的异常。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 指定参数名来调用函数"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们前面说过，调用函数时传入的参数值会严格按照顺序去匹配参数变量，第一个输入值赋给第一个参数变量，第二个值赋给第二个参数变量，依此类推。因为有了上面说的带缺省值参数，这个规则出现了变通的可能。\n",
    "\n",
    "如果一个函数有多个带缺省值的参数，我们想忽略掉某几个参数（就用其缺省值），但指定后面某一个参数的值（覆盖缺省值），例如下面这个函数："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "def greeting(name, msg='Hi', punc='!'):\n",
    "    print(f'{msg}, {name}{punc}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在这个版本的 `greeting()` 函数中，包含一个普通参数 `name` 和两个带缺省值的参数 `msg` `punc`，如果我们想跳过 `msg` 只传入 `name`（这个是必须的，因为没有缺省值）和 `punc` 的值，那么就可用下面的语法："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Hi, Neo.\n"
     ]
    }
   ],
   "source": [
    "greeting('Neo', punc='.')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这里第一个值按照顺序位置匹配到参数变量 `name`，这叫 *positional argument*（即“按照位置顺序匹配的参数”），而按照位置下一个是 `msg`，是我们想跳过的，所以要注明参数变量名，说明下一个传入的值 `'.'` 是给 `punc` 参数变量的，这叫 *keyword argument*（即“按照参数名匹配的参数”）。\n",
    "\n",
    "由于所有带缺省值的参数都在普通参数的后面，所以我们只要记住：\n",
    "* 调用函数时先传入所有不带缺省值的参数的值，严格按照函数定义的位置顺序（*positional*）；\n",
    "* 然后想指定哪些带缺省值参数的值，就用 `变量名=值` 这样的格式在后面列出（*keyword*），未列出的就还用缺省值了。\n",
    "\n",
    "在后半部分，顺序就无所谓了，可以和定义时不一样，反正是用名字指定的（*keyword*），比如我们完全可以这么干："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Good nite, Neo.\n"
     ]
    }
   ],
   "source": [
    "greeting('Neo', punc='.', msg='Good nite')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 变长参数"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "到目前为止，Python 的函数定义还是很简单清晰的，无论参数还是返回值，都没什么难懂的。下面开始就要进入比较混沌的领域了。\n",
    "\n",
    "所谓变长参数就是函数定义时名字前面带个星号 `*` 的参数变量，这表示这个变量其实是一组值，多少个都可以。我们先来看个简单的例子："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "def say_hi(*names):\n",
    "    for name in names:\n",
    "        print('Hi,', name)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Hi, Neo\n"
     ]
    }
   ],
   "source": [
    "say_hi('Neo')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Hi, Neo\n",
      "Hi, Trinity\n"
     ]
    }
   ],
   "source": [
    "say_hi('Neo', 'Trinity')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Hi, Neo\n",
      "Hi, Trinity\n",
      "Hi, Morpheus\n"
     ]
    }
   ],
   "source": [
    "say_hi('Neo', 'Trinity', 'Morpheus')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在这个例子里，`*names` 是一个变长参数（*arbitrary argument*），调用时可以传入一个或者多个值，函数会把这些值看做一个列表，赋给局部变量 `names`——后面我们会知道，其实不是**列表**（*list*），而是一个**元组**（*tuple*）——然后我们在函数体中可以用 `for...in` 来对这个 `names` 做循环。\n",
    "\n",
    "> 有些中文书籍把 *arbitrary arguments* 翻译成“可变参数”或者“任意参数”。事实上，在这样的地方，无论怎样的中文翻译都是很难准确表达原意的。这还算好的，甚至还见过翻译成“武断的参数”的——这样的翻译肯定会使读者产生说不明道不白的疑惑。\n",
    ">\n",
    "> 所以，**入门之后就尽量只用英文**是个好策略。虽然刚开始有点吃力，但后面会很省心，很长寿——是呀，少浪费时间、少浪费生命，其实就相当于更长寿了呀！"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在使用 *arbitrary argument* 的场合，有几点需要注意：\n",
    "* 参数变量名最好用复数单词，一看就知道是一组数据；这个变量在函数里通常都会被 `for...in` 循环处理，用复数名词在写类似 `for name in names` 的循环语句时会很舒服、很地道（*idiomatic*），是的，写程序和学外语一样，不写则已，写就要尽量写得“地道”；\n",
    "* 这种参数变量只能有一个，因为从它开始后面的输入值都会被当做它的一部分，多了就不知道怎么分了，显然，如果有这种参数，必须放在参数表的最后。\n",
    "\n",
    "上面的第二点，有一个不太常见的例外，那就是一个函数既有 *arbitrary arguments* 又有 *arguments with default values* 的情况，那么可以有两个 *arbitrary arguments*，其中第二个必须带缺省值，然后参数表排列成这样：\n",
    "\n",
    "`def monstrosity(*normal arguments*, *normal arbitrary argument*, *arguments with defaults*, *arbitrary argument with default*)`\n",
    "\n",
    "这样是完全符合语法要求的，调用时传入参数值还是按照前面讲的规则，先按照位置顺序匹配前两部分，多出来的都归 *normal arbitrary argument*；然后按照参数变量名指定对应值，没指定的都用缺省值。不过这实在是太麻烦了，不知道什么情况下才必须用这么可怕的函数，还是祈祷我们不会碰到这样的场景吧！\n",
    "\n",
    "当然，只有上面列出的前三个部分的情况还是有的，比如下面的例子："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [],
   "source": [
    "def say_hi(*names, msg='Hi', punc='!'):\n",
    "    for name in names:\n",
    "        print(f'{msg}, {name}{punc}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Hi, Neo.\n",
      "Hi, Trinity.\n"
     ]
    }
   ],
   "source": [
    "say_hi('Neo', 'Trinity', punc='.')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 小结"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "* 函数定义四要素：函数名、参数表、函数体和返回值，本章对每一个部分都进行了更深入的说明，尤其是一些特殊的用法；\n",
    "* 函数定义内外是两个不同的“**作用域**（*scope*）”，区分出全局变量和局部变量，需要充分理解其运作原理；\n",
    "* 参数表可以分为四段（正常情况下最多只会用到前三段），需要充分理解每一段的特点，如何定义和使用，以及为什么。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
